
package com.jf.cloud.common.security.filter;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.jf.cloud.api.auth.bo.UserInfoInTokenBO;
import com.jf.cloud.api.auth.constant.SysTypeEnum;
import com.jf.cloud.api.auth.feign.TokenFeignClient;
import com.jf.cloud.api.rbac.constant.HttpMethodEnum;
import com.jf.cloud.api.rbac.feign.PermissionFeignClient;
import com.jf.cloud.common.constant.StatusEnum;
import com.jf.cloud.common.exception.LuckException;
import com.jf.cloud.common.feign.FeignInsideAuthConfig;
import com.jf.cloud.common.handler.HttpHandler;
import com.jf.cloud.common.response.ResponseEnum;
import com.jf.cloud.common.response.ServerResponseEntity;
import com.jf.cloud.common.security.AuthUserContext;
import com.jf.cloud.common.security.adapter.AuthConfigAdapter;
import com.jf.cloud.common.security.adapter.DefaultSignAuthAdapter;
import com.jf.cloud.common.security.bo.SignResponse;
import com.jf.cloud.common.security.utils.SignUtils;
import com.jf.cloud.common.security.wrapper.ResponseWrapper;
import com.jf.cloud.common.util.IpHelper;
import com.jf.cloud.common.util.Json;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 授权过滤，只要实现AuthConfigAdapter接口，添加对应路径即可：
 *
 * @author zz
 * @date 2020/7/11
 */
@Component
public class AuthFilter implements Filter {

    private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);

    @Autowired
    private AuthConfigAdapter authConfigAdapter;

    @Autowired
    private HttpHandler httpHandler;

    @Autowired
    private TokenFeignClient tokenFeignClient;

    @Autowired
    private PermissionFeignClient permissionFeignClient;

    @Autowired
    private FeignInsideAuthConfig feignInsideAuthConfig;

    @Autowired
    private SignUtils signUtils;

    @Autowired
    private DefaultSignAuthAdapter defaultSignAuthAdapter;

    @Value("${sa-token.token-name}")
    private String tokenName;

    /**
     * 成功状态码
     */
    public static final int SUCCESS_CODE = 200;


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String requestUri = req.getRequestURI();
        if (!feignRequestCheck(req)) {
            httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
            return;
        }
        Set<String> excludePathPatterns = authConfigAdapter.excludePathPatterns();
        AntPathMatcher pathMatcher = new AntPathMatcher();
        // 如果匹配不需要授权的路径，就不需要校验是否需要授权
        if (CollectionUtil.isNotEmpty(excludePathPatterns)) {
            for (String excludePathPattern : excludePathPatterns) {
                if (pathMatcher.match(excludePathPattern, requestUri)) {
                    chain.doFilter(req, resp);
                    return;
                }
            }
        }
        String accessToken = req.getHeader(tokenName);
        String grantType = req.getHeader(SignUtils.GRANT_TYPE);
        // 也许需要登录，不登陆也能用的uri
        // 比如优惠券接口，登录的时候可以判断是否已经领取过
        // 不能登录的时候会看所有的优惠券，等待领取的时候再登录
        boolean mayAuth = pathMatcher.match(AuthConfigAdapter.MAYBE_AUTH_URI, requestUri);
        UserInfoInTokenBO userInfoInToken = null;
        try {
            // 通过签名访问
            if (Objects.equals(grantType, SignUtils.GRANT_TYPE_VALUE)) {
                signAccess(chain, req, resp);
            }
            // 如果有token，就要获取token
            else if (StrUtil.isNotBlank(accessToken)) {
                // 校验登录，并从缓存中取出用户信息
                try {
                    StpUtil.checkLogin();
                } catch (Exception e) {
                    httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
                    return;
                }
                ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenVoServerResponseEntity = checkUriAndGetToken(req, accessToken, mayAuth, pathMatcher);
                if (!userInfoInTokenVoServerResponseEntity.isSuccess()) {
                    httpHandler.printServerResponseToWeb(userInfoInTokenVoServerResponseEntity);
                    return;
                }
                userInfoInToken = userInfoInTokenVoServerResponseEntity.getData();

            }else if (!mayAuth) {
                httpHandler.printServerResponseToWeb(ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED));
                return;
            }
            // 保存上下文
            AuthUserContext.set(userInfoInToken);
            chain.doFilter(req, resp);
        }
        catch (Exception e) {
            if (e instanceof LuckException) {
                httpHandler.printServerResponseToWeb((LuckException) e);
                return;
            }
            throw e;
        }
        finally {
            AuthUserContext.clean();
        }

    }

    /**
     * 根据不同平台的token判断是否有url的权限
     */
    private ServerResponseEntity<UserInfoInTokenBO> checkUriAndGetToken(HttpServletRequest req, String accessToken, boolean mayAuth, AntPathMatcher pathMatcher) {
        // 校验token，并返回用户信息
        ServerResponseEntity<UserInfoInTokenBO> userInfoInTokenVoServerResponseEntity = tokenFeignClient
                .checkToken(accessToken);
        if (!userInfoInTokenVoServerResponseEntity.isSuccess() && !mayAuth) {
            return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
        }
        UserInfoInTokenBO userInfoInToken = userInfoInTokenVoServerResponseEntity.getData();
        String requestUri = req.getRequestURI();

        // 退出登录的url是统一的
        if (userInfoInToken != null && !pathMatcher.match(AuthConfigAdapter.LOGIN_OUT_URI, requestUri)) {
            boolean isFeign = pathMatcher.match(AuthConfigAdapter.FEIGN_URI, requestUri);
            boolean isPlatform = pathMatcher.match(AuthConfigAdapter.PLATFORM_URI, requestUri);
            boolean isMultiShop = pathMatcher.match(AuthConfigAdapter.MULTISHOP_URI, requestUri);
            boolean isSupplier = pathMatcher.match(AuthConfigAdapter.SUPPLIER_URI, requestUri);
            boolean isStation = Objects.equals(SysTypeEnum.STATION.value(), userInfoInToken.getSysType());
            boolean isPlatformOrMultiShop = pathMatcher.match(AuthConfigAdapter.MULTISHOP_PLATFORM_URI, requestUri);
            // 平台端的uri问题
            if (isPlatform && !Objects.equals(SysTypeEnum.PLATFORM.value(), userInfoInToken.getSysType())) {
                return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
            }

            // 商家端的uri问题
            if (isMultiShop) {
                if (!Objects.equals(SysTypeEnum.MULTISHOP.value(), userInfoInToken.getSysType())) {
                    return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
                }
                if (Objects.equals(userInfoInToken.getIsPassShop(), StatusEnum.DISABLE.value()) && !pathMatcher.match(AuthConfigAdapter.MULTSHOP_APPLY_URI, requestUri)) {
                    // 当商家处于申请开店状态并且调用不属于开店接口时
                    throw new LuckException("你没有权限访问该页面");
                }
            }
            // 供应商端的uri问题
            if (isSupplier) {
                if (!Objects.equals(SysTypeEnum.SUPPLIER.value(), userInfoInToken.getSysType())) {
                    return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
                }
                if (Objects.equals(userInfoInToken.getIsPassShop(), StatusEnum.DISABLE.value()) && !pathMatcher.match(AuthConfigAdapter.SUPPLIER_APPLY_URI, requestUri)) {
                    // 当供应商处于申请开店状态并且调用不属于开店接口时
                    throw new LuckException("你没有权限访问该页面");
                }
            }
            // 商家或平台的uri,TODO 缺供应商
            if (isPlatformOrMultiShop && !Objects.equals(SysTypeEnum.PLATFORM.value(), userInfoInToken.getSysType())
                    && !Objects.equals(SysTypeEnum.MULTISHOP.value(), userInfoInToken.getSysType())
                    && !Objects.equals(SysTypeEnum.SUPPLIER.value(), userInfoInToken.getSysType())) {
                return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
            }

            // 平台和商家和供应商还需要校验用户角色权限
            if (isPlatform || isMultiShop || isSupplier) {
                // 需要用户角色权限，就去根据用户角色权限判断是否
                if (!checkRbac(userInfoInToken, requestUri, req.getMethod())) {
                    return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
                }
            }

            // 用户端的uri问题
            if (!isPlatform && !isMultiShop && !isSupplier && !isPlatformOrMultiShop
                    && !isStation && !isFeign
                    && !Objects.equals(SysTypeEnum.ORDINARY.value(), userInfoInToken.getSysType())) {
                return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
            }
        }
        return ServerResponseEntity.success(userInfoInToken);
    }

    private boolean feignRequestCheck(HttpServletRequest req) {
        // 不是feign请求，不用校验
        if (!req.getRequestURI().startsWith(FeignInsideAuthConfig.FEIGN_URL_PREFIX)) {
            return true;
        }
        String feignInsideSecret = req.getHeader(feignInsideAuthConfig.getKey());

        // 校验feign 请求携带的key 和 value是否正确
        if (StrUtil.isBlank(feignInsideSecret) || !Objects.equals(feignInsideSecret, feignInsideAuthConfig.getSecret())) {
            return false;
        }
        // ip白名单
        List<String> ips = feignInsideAuthConfig.getIps();
        // 移除无用的空ip
        ips.removeIf(StrUtil::isBlank);
        // 有ip白名单，且ip不在白名单内，校验失败
        if (CollectionUtil.isNotEmpty(ips)
                && !ips.contains(IpHelper.getIpAddr())) {
            logger.error("ip not in ip White list: {}, ip, {}", ips, IpHelper.getIpAddr());
            return false;
        }
        return true;
    }


    /**
     * 用户角色权限校验
     *
     * @param uri uri
     * @return 是否校验成功
     */
    public boolean checkRbac(UserInfoInTokenBO userInfoInToken, String uri, String method) {
        ServerResponseEntity<Boolean> booleanServerResponseEntity = permissionFeignClient
                .checkPermission(userInfoInToken.getUserId(), userInfoInToken.getSysType(), uri, userInfoInToken.getIsAdmin(), HttpMethodEnum.valueOf(method.toUpperCase()).value());

        if (!booleanServerResponseEntity.isSuccess()) {
            return false;
        }

        return booleanServerResponseEntity.getData();
    }

    private void signAccess(FilterChain chain, HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        AntPathMatcher pathMatcher = new AntPathMatcher();
        UserInfoInTokenBO userInfoInToken;
        if (pathMatcher.match(AuthConfigAdapter.FEIGN_URI, req.getRequestURI())) {
            // feign校验
            Map<String, Object> dataMap = getFeignMap(req);
            ServerResponseEntity<UserInfoInTokenBO> tokenResponse = defaultSignAuthAdapter.loadUserInfoInToken(dataMap);
            userInfoInToken = tokenResponse.getData();
            // 保存上下文
            AuthUserContext.set(userInfoInToken);
            chain.doFilter(req, resp);
            return;
        }
        ServerResponseEntity<SignResponse> verifyResponse = signUtils.verify(req);
        SignResponse signResponse = verifyResponse.getData();
        long timestamp = System.currentTimeMillis();

        if (!verifyResponse.isSuccess()) {
            verifyResponse.setSign(SignUtils.sign(signResponse.getAppSecret(), timestamp, null));
            verifyResponse.setTimestamp(timestamp);
            verifyResponse.setData(null);
            ServerResponseEntity<String> response = ServerResponseEntity.success(Json.toJsonString(verifyResponse));
            response.setSign(verifyResponse.getSign());
            httpHandler.printServerResponseToWeb(response);
            return;
        }
        ServerResponseEntity<UserInfoInTokenBO> tokenResponse = defaultSignAuthAdapter.loadUserInfoInToken(signResponse.getDataMap());
        if (!tokenResponse.isSuccess()) {
            verifyResponse.setSign(SignUtils.sign(signResponse.getAppSecret(), timestamp, null));
            verifyResponse.setTimestamp(timestamp);
            verifyResponse.setData(null);
            verifyResponse.setMsg(tokenResponse.getMsg());
            ServerResponseEntity<String> response = ServerResponseEntity.success(Json.toJsonString(verifyResponse));
            response.setSign(verifyResponse.getSign());
            httpHandler.printServerResponseToWeb(response);
            return;
        }

        userInfoInToken = tokenResponse.getData();
        // 保存上下文
        AuthUserContext.set(userInfoInToken);
        ResponseWrapper responseWrapper = new ResponseWrapper(resp);
        chain.doFilter(signResponse.getReq(), responseWrapper);
        byte[] content = responseWrapper.getContent();
        // 获取相应数据
        String data = null;
        if (content.length > 0) {
            data = new String(content, StandardCharsets.UTF_8);
        }

        ServerResponseEntity<String> successResponse;
        if (StrUtil.isNotBlank(data)) {
            successResponse = Json.parseObject(data, ServerResponseEntity.class);
        } else if (responseWrapper.getStatus() != SUCCESS_CODE) {
            successResponse = ServerResponseEntity.showFailMsg(data);
        } else {
            successResponse = ServerResponseEntity.success(data);
        }

        String responseData = JSONObject.toJSONString(successResponse.getData());

        successResponse.setSign(SignUtils.sign(signResponse.getAppSecret(), timestamp, responseData));
        successResponse.setTimestamp(timestamp);
        successResponse.setData(responseData);
        httpHandler.printServerResponseToWeb(successResponse);
    }

    private static Map<String, Object> getFeignMap(HttpServletRequest req) {
        Map<String, Object> dataMap = new HashMap<>(6);
        dataMap.put("sysType", req.getHeader("sysType"));
        dataMap.put("sysUserId", req.getHeader("sysUserId"));
        dataMap.put("shopId", req.getHeader("shopId"));
        dataMap.put("userId", req.getHeader("userId"));
        dataMap.put("supplierId", req.getHeader("supplierId"));
        dataMap.put("stationId", req.getHeader("stationId"));
        return dataMap;
    }
}
